import greenfoot.*;  // (World, Actor, GreenfootImage, Greenfoot and MouseInfo)
import java.util.*;
import java.lang.reflect.*;

/**
 * This class represents The Greenfooted Salesman.
 * <br /><br />
 * It is responsible for accuratly responding to and tracking the Players moves.
 * 
 * @author (Dilyan GEORGIEV)
 * @author (Thomas KOBER)
 * @auther (Matthew MCDONALD)
 * @version (1.0)
 */
public class Salesman extends Actor implements Observable
{
    // Singleton Instance
    private static Salesman instance = null;
    
    ////////////////////////////////////////////////////////////////////////////////
    // Field declarations
    ////////////////////////////////////////////////////////////////////////////////
    
    // Tracking the Players Travel Expenses a.k.a the Cost
    private int                     travelExpenses;
    // Tracking the Players surplus Expenses
    private int                     surplusExpenses;
    // The Salesman's current position
    private SpaceNode<Planet>       currentPlanet;
    // List of Observers
    private ArrayList<GameObserver> observers;
    
    ////////////////////////////////////////////////////////////////////////////////
    // Constructor(s)
    ////////////////////////////////////////////////////////////////////////////////
    
    /**
     * Constructor for objects of class Salesman.
     */
    private Salesman()
    {
        super();
        
        this.travelExpenses = 0;
        this.surplusExpenses = 0;
        
        this.observers = new ArrayList<GameObserver>();
    }
    
    ////////////////////////////////////////////////////////////////////////////////
    // Public Implementation
    ////////////////////////////////////////////////////////////////////////////////
    
    /**
     * Returns the Singleton instance of the Salesman.
     * @return the Singleton instance of the Salesman.
     */
    public static Salesman getInstance()
    {
        if (instance == null) {
            instance = new Salesman();
        }
        
        return instance;
    }
    
    /**
     * Resets the Salesman instance.
     */
    public void resetSalesman()
    {
        this.travelExpenses = 0;
        this.surplusExpenses = 0;
        this.currentPlanet = null;
    }
    
    /**
     * Act - do whatever the Salesman wants to do. This method is called whenever
     * the 'Act' or 'Run' button gets pressed in the environment.
     */
    public void act() 
    {
        if (!((SalesWorld)super.getWorld()).isPaused()) {
            Class[] params = null;
            Object[] methodParams = null;
            
            SpaceNode<Planet> nextPlanet = null;
            int xDiff = 0;
            int yDiff = 0;
            
            if (Greenfoot.isKeyDown("up")) {
                try {
                    this.notifyObservers(GameObserver.class.getDeclaredMethod("handlePlayerMoved", params), methodParams);
                } catch (NoSuchMethodException ex) {
                    ex.printStackTrace();
                }
            
                nextPlanet = ((SalesWorld)super.getWorld()).getSpaceTree().getRight(this.currentPlanet);
                
            } else if (Greenfoot.isKeyDown("down")) {
                try {
                    this.notifyObservers(GameObserver.class.getDeclaredMethod("handlePlayerMoved", params), methodParams);
                } catch (NoSuchMethodException ex) {
                    ex.printStackTrace();
                }
                
                nextPlanet = ((SalesWorld)super.getWorld()).getSpaceTree().getLeft(this.currentPlanet);
            } else if (Greenfoot.isKeyDown("left")) {
                try {
                    this.notifyObservers(GameObserver.class.getDeclaredMethod("handlePlayerMoved", params), methodParams);
                } catch (NoSuchMethodException ex) {
                    ex.printStackTrace();
                }
                
                nextPlanet = ((SalesWorld)super.getWorld()).getSpaceTree().getParent(this.currentPlanet);
                
                this.surplusExpenses += nextPlanet.getData().getCost().getValue();
            }
            
            if (nextPlanet != null) {
             
                xDiff = nextPlanet.getData().getX() - this.currentPlanet.getData().getX();
                yDiff = nextPlanet.getData().getY() - this.currentPlanet.getData().getY();
                
                super.setLocation(super.getX() + xDiff, super.getY() + yDiff);
                
                this.travelExpenses += nextPlanet.getData().getCost().getValue();
                
                this.currentPlanet = nextPlanet;
                
                try {
                    this.notifyObservers(GameObserver.class.getDeclaredMethod("handleCostIncrease", params), methodParams);
                } catch (NoSuchMethodException ex) {
                    ex.printStackTrace();
                }
                
                this.checkGoalPlanet();
            }
        }
    }
    
    /**
     * Returns the current Travel Expenses, a.k.a the Cost.
     * @return the current Travel Expenses.
     */
    public int getTravelExpenses()
    {
        return this.travelExpenses;
    }
    
    /**
     * Returns the Surplus Travel Expenses.
     * @return the Surplus Travel Expenses.
     */
    public int getSurplusExpenses()
    {
        return this.surplusExpenses;
    }
    
    /**
     * Beams the Salesman to the given Planet
     * @param planet the planet to beam the Salesman to.
     */
    public void setCurrentPlanet(SpaceNode planet)
    {
        this.currentPlanet = planet;
    }
    
    /**
     * Returns the Salesman's current position.
     * @return the Salesman's current position.
     */
    public SpaceNode getCurrentPlanet()
    {
        return this.currentPlanet;
    }
    
    ////////////////////////////////////////////////////////////////////////////////
    // Observable Interface Implementation
    ////////////////////////////////////////////////////////////////////////////////
    
    /**
     * Adds the given GameObserver to the List of Observers of this object.
     * @param o the GameObserver to be added to the List of Observers.
     */
    public void addObserver(GameObserver o)
    {
        this.observers.add(o);
    }
    
    /**
     * Removes the given GameObserver from the List of Observers of this object.
     * @param o the GameObserver to be removed from the List of Observers.
     */
    public void removeObserver(GameObserver o)
    {
        this.observers.remove(o);
    }
    
    /**
     * Notifies the Objects' Observers about an update.
     * @param theMethod the update Method to be invoked.
     * @param args the corresponding Parameter List of theMethod.
     */
    public void notifyObservers(Method theMethod, Object... args)
    {
        for (GameObserver o : this.observers) {
            try {
                if (args != null) {
                    theMethod.invoke(o, args);
                } else {
                    theMethod.invoke(o);
                }
            } catch (IllegalAccessException ex) {
                ex.printStackTrace();
            } catch (InvocationTargetException ex) {
                ex.printStackTrace();
            }
        }
    }
    
    ////////////////////////////////////////////////////////////////////////////////
    // Private Implementation
    ////////////////////////////////////////////////////////////////////////////////
    
    /**
     * Checks if the Current Planet is a Goal Planet and triggers an Observer Update in case it is.
     */
    private void checkGoalPlanet()
    {
        Class[] params = null;
        Object[] methodParams = null;
        if (this.currentPlanet.getData().isGoalPlanet()) {
            try {
                this.notifyObservers(GameObserver.class.getDeclaredMethod("handlePlayerReachedGoalPlanet", params), methodParams);
            } catch (NoSuchMethodException ex) {
                ex.printStackTrace();
            }
        }
    }
}
